/**
 * Copyright (C) 2010 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.everrest.groovy;

import groovy.lang.GroovyCodeSource;

import org.codehaus.groovy.control.CompilationFailedException;
import org.everrest.core.DependencySupplier;
import org.everrest.core.ObjectFactory;
import org.everrest.core.PerRequestObjectFactory;
import org.everrest.core.ResourceBinder;
import org.everrest.core.ResourcePublicationException;
import org.everrest.core.resource.AbstractResourceDescriptor;
import org.everrest.core.uri.UriPattern;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;

/**
 * Manage via {@link ResourceBinder} Groovy based RESTful services.
 *
 * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
 * @version $Id$
 */
public class GroovyResourcePublisher
{
   /** Default character set name. */
   protected static final String DEFAULT_CHARSET_NAME = "UTF-8";

   /** Default character set. */
   protected static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_CHARSET_NAME);

   protected final ResourceBinder binder;

   protected final Map<ResourceId, String> resources = Collections.synchronizedMap(new HashMap<ResourceId, String>());

   protected final GroovyClassLoaderProvider classLoaderProvider;

   protected final DependencySupplier dependencies;

   protected final Comparator<Constructor<?>> CONSTRUCTOR_COMPARATOR = new Comparator<Constructor<?>>()
   {
      public int compare(Constructor<?> o1, Constructor<?> o2)
      {
         return o2.getParameterTypes().length - o1.getParameterTypes().length;
      }
   };

   /**
    * Create GroovyJaxrsPublisher which is able publish per-request and
    * singleton resources. Any required dependencies for per-request resource
    * injected by {@link PerRequestObjectFactory}.
    *
    * @param binder resource binder
    * @param classLoaderProvider GroovyClassLoaderProvider
    * @param dependencies dependencies resolver
    * @see DependencySupplier
    */
   protected GroovyResourcePublisher(ResourceBinder binder, GroovyClassLoaderProvider classLoaderProvider,
                                     DependencySupplier dependencies)
   {
      this.binder = binder;
      this.classLoaderProvider = classLoaderProvider;
      this.dependencies = dependencies;
   }

   /**
    * Create GroovyJaxrsPublisher which is able publish per-request and
    * singleton resources. Any required dependencies for per-request resource
    * injected by {@link PerRequestObjectFactory}.
    *
    * @param binder resource binder
    * @param dependencies dependencies resolver
    * @see DependencySupplier
    */
   public GroovyResourcePublisher(ResourceBinder binder, DependencySupplier dependencies)
   {
      this(binder, new GroovyClassLoaderProvider(), dependencies);
   }

   /**
    * Get resource corresponded to specified id <code>resourceId</code> .
    *
    * @param resourceId resource id
    * @return resource or <code>null</code>
    */
   public ObjectFactory<AbstractResourceDescriptor> getResource(ResourceId resourceId)
   {
      String path = resources.get(resourceId);
      if (path == null)
      {
         return null;
      }

      UriPattern pattern = new UriPattern(path);
      for (ObjectFactory<AbstractResourceDescriptor> res : binder.getResources())
      {
         if (res.getObjectModel().getUriPattern().equals(pattern))
         {
            return res;
         }
      }
      // If resource not exists any more but still in mapping.
      resources.remove(resourceId);
      return null;
   }

   /**
    * Check is groovy resource with specified id is published or not
    *
    * @param resourceId id of resource to be checked
    * @return <code>true</code> if resource is published and <code>false</code>
    *         otherwise
    */
   public boolean isPublished(ResourceId resourceId)
   {
      return null != getResource(resourceId);
   }

   /**
    * Parse given stream and publish result as per-request RESTful service.
    *
    * @param in stream which contains groovy source code of RESTful service
    * @param resourceId id to be assigned to resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Class, MultivaluedMap)}
    * @throws CompilationFailedException if compilation fails from source errors
    */
   public void publishPerRequest(InputStream in, ResourceId resourceId, MultivaluedMap<String, String> properties,
                                 SourceFolder[] src, SourceFile[] files)
   {
      Class<?> rc;
      try
      {
         rc = classLoaderProvider.getGroovyClassLoader(src).parseClass(in, resourceId.getId(), files);
      }
      catch (MalformedURLException e)
      {
         throw new IllegalArgumentException(e.getMessage());
      }
      binder.addResource(rc, properties);
      resources.put(resourceId, rc.getAnnotation(Path.class).value());
   }

   /**
    * Parse given <code>source</code> and publish result as per-request RESTful
    * service.
    *
    * @param source groovy source code of RESTful service
    * @param resourceId id to be assigned to resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Class, MultivaluedMap)}
    */
   public final void publishPerRequest(String source, ResourceId resourceId, MultivaluedMap<String, String> properties,
                                       SourceFolder[] src, SourceFile[] files)
   {
      publishPerRequest(source, DEFAULT_CHARSET, resourceId, properties, src, files);
   }

   /**
    * Parse given <code>source</code> and publish result as per-request RESTful
    * service.
    *
    * @param source groovy source code of RESTful service
    * @param charset source string charset. May be <code>null</code> than
    * default charset will be in use
    * @param resourceId id to be assigned to resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>.
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws UnsupportedCharsetException if <code>charset</code> is unsupported
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Class, MultivaluedMap)}
    */
   public final void publishPerRequest(String source, String charset, ResourceId resourceId,
                                       MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files)
   {
      publishPerRequest(source, charset == null ? DEFAULT_CHARSET : Charset.forName(charset), resourceId, properties,
         src, files);
   }

   /**
    * Parse given stream and publish result as singleton RESTful service.
    *
    * @param in stream which contains groovy source code of RESTful service
    * @param resourceId id to be assigned to resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Object, MultivaluedMap)}
    */
   public void publishSingleton(InputStream in, ResourceId resourceId, MultivaluedMap<String, String> properties,
                                SourceFolder[] src, SourceFile[] files)
   {
      Class<?> rc;
      try
      {
         rc = classLoaderProvider.getGroovyClassLoader(src).parseClass(in, resourceId.getId(), files);
      }
      catch (MalformedURLException e)
      {
         throw new IllegalArgumentException(e.getMessage());
      }

      Object r;
      try
      {
         r = createInstance(rc);
      }
      catch (IllegalArgumentException e)
      {
         throw new ResourcePublicationException(e.getMessage());
      }
      catch (InstantiationException e)
      {
         throw new ResourcePublicationException(e.getMessage());
      }
      catch (IllegalAccessException e)
      {
         throw new ResourcePublicationException(e.getMessage());
      }
      catch (InvocationTargetException e)
      {
         throw new ResourcePublicationException(e.getMessage());
      }

      binder.addResource(r, properties);
      resources.put(resourceId, r.getClass().getAnnotation(Path.class).value());
   }

   /**
    * Parse given <code>source</code> and publish result as singleton RESTful
    * service.
    *
    * @param source groovy source code of RESTful service
    * @param resourceId name of resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>.
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Object, MultivaluedMap)}
    */
   public final void publishSingleton(String source, ResourceId resourceId, MultivaluedMap<String, String> properties,
                                      SourceFolder[] src, SourceFile[] files)
   {
      publishSingleton(source, DEFAULT_CHARSET, resourceId, properties, src, files);
   }

   /**
    * Parse given <code>source</code> and publish result as singleton RESTful
    * service.
    *
    * @param source groovy source code of RESTful service
    * @param charset source string charset. May be <code>null</code> than
    * default charset will be in use
    * @param resourceId name of resource
    * @param properties optional resource properties. This parameter may be
    * <code>null</code>.
    * @param src additional path to Groovy sources
    * @param files Groovy source files to be added in build path directly
    * @throws UnsupportedCharsetException if <code>charset</code> is unsupported
    * @throws NullPointerException if <code>resourceId == null</code>
    * @throws ResourcePublicationException see
    * {@link ResourceBinder#addResource(Object, MultivaluedMap)}
    */
   public final void publishSingleton(String source, String charset, ResourceId resourceId,
                                      MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files)
   {
      publishSingleton(source, charset == null ? DEFAULT_CHARSET : Charset.forName(charset), resourceId, properties,
         src, files);
   }

   /**
    * Unpublish resource with specified id.
    *
    * @param resourceId id of resource to be unpublished
    * @return <code>true</code> if resource was published and <code>false</code>
    *         otherwise, e.g. because there is not resource corresponded to
    *         supplied <code>resourceId</code>
    */
   public ObjectFactory<AbstractResourceDescriptor> unpublishResource(ResourceId resourceId)
   {
      String path = resources.get(resourceId);
      if (path == null)
      {
         return null;
      }
      ObjectFactory<AbstractResourceDescriptor> resource = binder.removeResource(path);
      if (resource != null)
      {
         resources.remove(resourceId);
      }
      return resource;
   }

   private void publishPerRequest(String source, Charset charset, ResourceId resourceId,
                                  MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files)
   {
      byte[] bytes = source.getBytes(charset);
      publishPerRequest(new ByteArrayInputStream(bytes), resourceId, properties, src, files);
   }

   private void publishSingleton(String source, Charset charset, ResourceId resourceId,
                                 MultivaluedMap<String, String> properties, SourceFolder[] src, SourceFile[] files)
   {
      byte[] bytes = source.getBytes(charset);
      publishSingleton(new ByteArrayInputStream(bytes), resourceId, properties, src, files);
   }

   protected Object createInstance(Class<?> clazz) throws IllegalArgumentException, InstantiationException,
      IllegalAccessException, InvocationTargetException
   {
      Constructor<?>[] constructors = clazz.getConstructors();
      //Sort constructors by number of parameters. With more parameters must be first.
      Arrays.sort(constructors, CONSTRUCTOR_COMPARATOR);
      l:
      for (Constructor<?> c : constructors)
      {
         Class<?>[] parameterTypes = c.getParameterTypes();
         if (parameterTypes.length == 0)
         {
            return c.newInstance();
         }
         Object[] parameters = new Object[parameterTypes.length];
         for (int i = 0; i < parameterTypes.length; i++)
         {
            Object param = dependencies.getComponent(parameterTypes[i]);
            if (param == null)
            {
               continue l;
            }
            parameters[i] = param;
         }
         return c.newInstance(parameters);
      }
      throw new ResourcePublicationException("Unable create instance of class " + clazz.getName()
         + ". Required constructor's dependencies can't be resolved. ");
   }

   /**
    * Create {@link GroovyCodeSource} from given stream and name. Code base
    * 'file:/groovy/script/jaxrs' will be used.
    *
    * @param in groovy source code stream
    * @param name code source name
    * @return GroovyCodeSource
    */
   protected GroovyCodeSource createCodeSource(InputStream in, String name)
   {
      GroovyCodeSource gcs =
         new GroovyCodeSource(new BufferedReader(new InputStreamReader(in)), name, "/groovy/script/jaxrs");
      gcs.setCachable(false);
      return gcs;
   }
}
